DELETE Product Event Endpoint API Documentation
Overview
Endpoint: DELETE /product-event/:id
Description: Soft delete an Event and all associated data including related Products and Posts.
Authentication: JWT Bearer Token (Global JwtAuthGuard)
Request Flow
1. Controller Layer (src/product-event/product.event.controller.ts)
Location: Lines 76-81
@Delete(':id')
async delete(@Param('id') id: string) {
const userId = this.requestContextService.getUserId();
await this.productEventService.delete(id, userId);
return 'ok';
}
Key Points:
- Route Parameter:
id- Event UUID - Authentication: Uses global
JwtAuthGuard(no explicit@UseGuardsdecorator) - User Context: Gets
userIdfromRequestContextService - Response: Returns string
'ok'on success
2. Service Layer (src/product-event/product.event.service.ts)
Location: Lines 1169-1187
async delete(id: string, curatorId: string) {
// Step 1: Verify event ownership
const entity = await this.findUniqueEventEntityOrThrow(id, curatorId);
// Step 2: Check for existing orders
const hasOrders = await this.repo.hasExistingOrders([id]);
if (hasOrders) {
throw new BadRequestException(
"This event can't be deleted because it already has customer orders.",
);
}
// Step 3: Get related product IDs
const productIds = entity.relatedProductIds();
// Step 4: Soft delete event and products
await this.repo.delete([id], productIds);
// Step 5: Delete related posts
await this.syncDeletePosts(entity);
// Step 6: Queue async processing
await this.productEventPublisher.onDelete(productIds);
}
Step 2a: Ownership Verification
Method: findUniqueEventEntityOrThrow()
Location: src/product-event-core/product-event-core.service.ts (Lines 93-109)
async findUniqueEventEntityOrThrow(id: string, curatorId?: string): Promise<EventEntity> {
const result = await this.findUniqueEventEntity(id, curatorId);
if (!result) {
throw new ResourceNotFound({ resource: 'Event', id });
}
return result;
}
Query: Filters by both id AND curatorId to ensure ownership
Step 2b: Order Existence Check
Method: hasExistingOrders()
Location: src/product-event/product.event.repo.ts (Lines 652-663)
async hasExistingOrders(eventIds: string[]) {
const orderLineItems = await this.prisma.orderLineItem.findMany({
where: {
unitPrice: { gt: 0 }, // Only paid orders count
eventId: { in: eventIds },
},
take: 1,
});
return orderLineItems.length > 0;
}
Business Rule: Events with paid orders (unitPrice > 0) cannot be deleted.
Step 3: Related Product IDs
Method: EventEntity.relatedProductIds()
Location: src/product-event/entity/product.event.entity.ts (Lines 238-240)
relatedProductIds(): string[] {
return this.relatedProduct()?.map((p) => p.id) ?? [];
}
Returns all Product IDs associated with the Event (ticket products, event products).
3. Repository Layer - Soft Delete (src/product-event/product.event.repo.ts)
Location: Lines 477-497
async delete(ids: string[], productIds: string[]) {
await this.prisma.$transaction(async (transaction) => {
// Soft delete Event(s)
await transaction.event.updateMany({
where: { id: { in: ids } },
data: { deletedAt: new Date() },
});
// Soft delete related Product(s)
isEmpty(productIds)
? null
: await transaction.product.updateMany({
where: { id: { in: productIds } },
data: {
deletedAt: new Date(),
isAvailable: false,
},
});
});
}
Transaction Scope: All database operations are atomic (all succeed or all fail).
Database Operations:
Event.deletedAt = NOW()- Soft delete eventProduct.deletedAt = NOW()- Soft delete productsProduct.isAvailable = false- Mark products unavailable
4. Post Deletion (src/product-event/product.event.service.ts)
Method: syncDeletePosts()
Location: Lines 1190-1203
private async syncDeletePosts(entity: EventEntity) {
const needDeletePostIds = (await this.repo.findPostsByEventId(entity)).map(
(post) => post.id,
);
await Promise.all([
...needDeletePostIds.map(async (postId) => {
return await this.postsCuratorService.deletePost(
entity.curatorId,
postId,
true,
);
}),
]);
}
Target Posts: All Posts where post.createFromEventId === event.id
Post Deletion Service (src/posts/curator/posts.curator.service.ts, Lines 1955-1971):
async deletePost(userId: string, postId: string, isChildPostExpired: boolean = false) {
const post = await this.postsService.findPostById(postId);
if (!post || post.deletedAt || post.creatorId !== userId) {
throw new ResourceNotFound({ postId });
}
const data = await this.repo.deletePost(postId, isChildPostExpired);
await this.postsPublisher.removePostCoupons(data.childPostIds.concat(postId));
return data.post;
}
Post Deletion Flow:
- Verify post ownership (
creatorId === userId) - Soft delete post (
deletedAt = NOW()) - Remove associated coupons
5. Queue Processing
Publisher (src/product-event/event/product.event.publisher.ts)
Location: Lines 65-72
async onDelete(productIds: string[]) {
if (isEmpty(productIds)) {
return;
}
await this.productEventQueue.add('onDelete', { productIds });
}
Queue: Event queue (Bull)
Job Name: onDelete
Job Data: { productIds: string[] }
Subscriber (src/product-event/event/product.event.subscriber.ts)
Location: Lines 34-47
@Process('onDelete')
async onDelete(job: Job<{ productIds: string[] }>) {
const { productIds } = job.data;
const products =
await this.merchantProductsRepository.findManyFromDbOnly(productIds);
await Promise.all(
products.map(async (product) => {
await this.merchantProductsService.afterProductDeleted(product);
}),
);
}
Processing: For each deleted Product, call afterProductDeleted()
Post-Delete Handler (src/products/merchant-products/merchant-products.service.ts)
Location: Lines 488-497
async afterProductDeleted(data: ProductComplete | null) {
if (data) {
await this.busService.emit(
EventNames.UpdateMerchantProduct,
data,
'delete',
);
}
return data;
}
Event Emitted: UpdateMerchantProduct with operation type 'delete'
Complete Flow Diagram
┌──────────────────────────────────────────────────────────────────────┐
│ DELETE /product-event/:id │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 1. Controller Layer │ │
│ │ - Extract userId from RequestContextService │ │
│ │ - Call productEventService.delete(id, userId) │ │
│ └───────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼─────────────────────────────────────┐ │
│ │ 2. Service Layer │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 2a. Verify Event Ownership │ │ │
│ │ │ - findUniqueEventEntityOrThrow(id, curatorId) │ │ │
│ │ │ - Throws ResourceNotFound if not owner │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 2b. Check for Existing Orders │ │ │
│ │ │ - hasExistingOrders([id]) │ │ │
│ │ │ - Throws BadRequestException if hasPaidOrders │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 2c. Get Related Product IDs │ │ │
│ │ │ - entity.relatedProductIds() │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 2d. Soft Delete (Database Transaction) │ │ │
│ │ │ - repo.delete([id], productIds) │ │ │
│ │ │ - Event.deletedAt = NOW() │ │ │
│ │ │ - Product.deletedAt = NOW() │ │ │
│ │ │ - Product.isAvailable = false │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 2e. Delete Related Posts │ │ │
│ │ │ - syncDeletePosts(entity) │ │ │
│ │ │ - Find posts with createFromEventId === event.id│ │ │
│ │ │ - Call postsCuratorService.deletePost() │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 2f. Queue Async Processing │ │ │
│ │ │ - productEventPublisher.onDelete(productIds) │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼─────────────────────────────────────┐ │
│ │ 3. Queue Processing (Async) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Subscriber.onDelete() │ │ │
│ │ │ - For each product: afterProductDeleted(product) │ │ │
│ │ │ - Emit UpdateMerchantProduct event (delete) │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ Response: 'ok' │
└──────────────────────────────────────────────────────────────────────┘
Business Rules
1. Ownership Requirement
- Rule: Only the Event owner (curatorId) can delete the Event
- Enforcement: Database query filters by both
idandcuratorId - Error:
ResourceNotFoundif Event doesn't exist or user doesn't own it
2. Existing Orders Protection
- Rule: Events with paid orders (
unitPrice > 0) cannot be deleted - Reason: Prevents data integrity issues with historical order data
- Error:
BadRequestExceptionwith message "This event can't be deleted because it already has customer orders." - Note: Free orders (
unitPrice = 0) do not prevent deletion
3. Soft Delete Pattern
- Event:
deletedAttimestamp set (not hard deleted from database) - Products: Both
deletedAtset ANDisAvailable = false - Reason: Preserves historical data while hiding from UI
4. Cascading Deletions
- Products: All related products (tickets, event products) soft deleted
- Posts: All Posts with
createFromEventId === event.idsoft deleted - Coupons: Associated coupons removed via Post deletion flow
5. Transaction Safety
- All database operations wrapped in Prisma transaction
- Either ALL succeed or ALL fail (atomic operation)
Request/Response Examples
Request
curl 'https://release.katana-api.1m.app/product-event/b4e38b06-7902-4ba6-ad7e-5429417927d3' \
-X 'DELETE' \
-H 'authorization: Bearer <JWT_TOKEN>' \
-H 'from: client'
Success Response
HTTP/1.1 200 OK
Content-Type: application/json
"ok"
Error Responses
Event Not Found / Not Owned:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"statusCode": 404,
"message": "Resource Event with id b4e38b06-7902-4ba6-ad7e-5429417927d3 not found"
}
Existing Orders:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"statusCode": 400,
"message": "This event can't be deleted because it already has customer orders."
}
Related Endpoints
GET /product-event/:id/has-existing-orders- Check if event has orders before attempting deletionGET /product-event/:id/details- Get event detailsGET /product-event/list- List user's events
Database Schema Affected
Event Table
UPDATE "Event" SET "deletedAt" = NOW() WHERE "id" = $1;
Product Table
UPDATE "Product"
SET "deletedAt" = NOW(), "isAvailable" = false
WHERE "id" = ANY($1);
Post Table
UPDATE "Post" SET "deletedAt" = NOW()
WHERE "createFromEventId" = $1;
Queue Configuration
Queue Name: Event
Job Options (from src/product-event/product.event.module.ts):
{
attempts: 3,
removeOnComplete: 100,
removeOnFail: 100,
backoff: {
type: 'exponential',
delay: 1000,
},
}
Rate Limiting: limiter: { max: 100, duration: 100 } (100 jobs per 100ms)
Testing Considerations
Unit Tests Needed
- Ownership Verification: Non-owner cannot delete event
- Order Protection: Event with orders cannot be deleted
- Soft Delete: Verify deletedAt timestamps set correctly
- Product Availability: Verify isAvailable = false on products
- Post Deletion: Verify related posts deleted
- Queue Processing: Verify onDelete event published
Integration Tests Needed
- Full Flow: Delete event → verify all cascading deletions
- Transaction Rollback: Verify rollback on partial failure
- Queue Processing: Verify async completion
- Cross-Module: Verify Post service integration
Monitoring & Logging
Key Logging Points
- Service Entry: Log delete request with eventId and userId
- Order Check: Log when deletion blocked due to existing orders
- Repository: Log transaction success/failure
- Post Deletion: Log number of posts deleted
- Queue: Log job added to queue
Error Monitoring
- Track
BadRequestExceptionfor order protection (may indicate UX issues) - Track
ResourceNotFoundfor ownership failures (potential security concerns) - Monitor queue job failures
References
- Controller:
src/product-event/product.event.controller.ts:76-81 - Service:
src/product-event/product.event.service.ts:1169-1187 - Repository:
src/product-event/product.event.repo.ts:477-497 - Publisher:
src/product-event/event/product.event.publisher.ts:65-72 - Subscriber:
src/product-event/event/product.event.subscriber.ts:34-47 - Post Service:
src/posts/curator/posts.curator.service.ts:1955-1971
Last Updated: 2026-02-25 Document Version: 1.0